// Copyright (c) 2008 screamingtarget@gmail.com
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.


#pragma once

#include <boost/asio.hpp >
#include <boost/bind.hpp>
#include <boost/pool/object_pool.hpp>
#include <deque>

#include "..\PortAudio\include\portaudio.h"


const UINT RWM_CLIENTSTARTED = RegisterWindowMessage( _T("RWM_CLIENTSTARTED__018EE6FF_95CA_4960_B444_882CB179C4A7") );
const UINT RWM_CLIENTCONNECTED = RegisterWindowMessage( _T("RWM_CLIENTCONNECTED__AE7D2349_33B8_4365_ADE6_B7ED17259F15") );
const UINT RWM_AUDIODROPOUT = RegisterWindowMessage( _T("RWM_AUDIODROPOUT__B5FF2BB4_3C37_47c5_A2B8_22A5D1ED6F6D") );
const UINT RWM_PORTAUDIOSTATUS = RegisterWindowMessage( _T("RWM_PORTAUDIOSTATUS__EE6D6A32_D732_4348_8653_FE91A54561E6") );


using boost::asio::ip::tcp;


class Client : private boost::noncopyable
{
public:

   Client( boost::asio::io_service& io_service, tcp::resolver::iterator endpoint_iterator, HWND hWnd )
      : io_service_( io_service ), socket_( io_service ), hwndNotify_( hWnd ), stream_( NULL )
   {
      startPlaying_ = false;

      tcp::endpoint endpoint = *endpoint_iterator;

      socket_.async_connect( endpoint,
         boost::bind( &Client::HandleConnect, this,
         boost::asio::placeholders::error, ++endpoint_iterator ) );

      ::PostMessage( hwndNotify_, RWM_CLIENTSTARTED, 0, 0 );
   }

   ~Client()
   {
      Cleanup();
   }

   void Stop()
   {
      io_service_.post( boost::bind( &Client::HandleStop, this ) );
   }

private:

   boost::asio::io_service& io_service_;
   tcp::socket socket_;

   HWND hwndNotify_;

   PaStream* stream_;
   PaError error_;
   PaStreamParameters outputParameters_;

   enum
   {
      SampleRate = 44100,
      NumChannels = 2,
      SampleSize = 2,
      FramesPerBuffer = 1024
   };

   struct ReadBuffer
   {
      enum { Size = NumChannels * SampleSize * FramesPerBuffer };
      byte data[Size];
   };

   boost::object_pool< ReadBuffer > pool_;
   std::deque< ReadBuffer* const > pending_;
   ReadBuffer* current_;

   bool startPlaying_;

   enum { BuffersBeforePlaying = 32 };

   void HandleConnect( const boost::system::error_code& error,
      tcp::resolver::iterator endpoint_iterator )
   {
      if ( !error )
      {
         ::PostMessage( hwndNotify_, RWM_CLIENTCONNECTED, 0, 0 );

         SetupPortAudio();

         current_ = pool_.malloc();

         boost::asio::async_read( socket_, 
            boost::asio::mutable_buffers_1( boost::asio::mutable_buffer( current_->data, ReadBuffer::Size ) ),
            boost::bind( &Client::HandleRead, this,
            boost::asio::placeholders::error ) );
      }
      else if ( endpoint_iterator != tcp::resolver::iterator() )
      {
         socket_.close();
         tcp::endpoint endpoint = *endpoint_iterator;
         socket_.async_connect( endpoint,
            boost::bind( &Client::HandleConnect, this,
            boost::asio::placeholders::error, ++endpoint_iterator ) );
      }
   }

   void HandleRead( const boost::system::error_code& error )
   {
      if ( !error )
      {
         pending_.push_back( current_ );

         if ( !startPlaying_ && pending_.size() > BuffersBeforePlaying )
         {
            startPlaying_ = true;
         }

         current_ = pool_.malloc();

         boost::asio::async_read( socket_,
            boost::asio::mutable_buffers_1( boost::asio::mutable_buffer( current_->data, ReadBuffer::Size ) ),
            boost::bind( &Client::HandleRead, this,
            boost::asio::placeholders::error ) );
      }
      else
      {
         HandleStop();
      }
   }

   void CleanupPending()
   {
      while ( !pending_.empty() )
      {
         ReadBuffer* buffer = pending_.front();
         pending_.pop_front();
         pool_.free( buffer );
      }
   }

   void HandleStop()
   {
      Pa_AbortStream( stream_ );
      io_service_.stop();
   }

   void Cleanup()
   {
      CleanupPortAudio();
      socket_.close();
      CleanupPending();
   }

   bool SetupPortAudio()
   {
      error_ = Pa_Initialize();

      if ( paNoError != error_ )
      {
         return false;
      }

      outputParameters_.device = Pa_GetDefaultOutputDevice();

      if ( paNoDevice == outputParameters_.device )
      {
         CleanupPortAudio();
         return false;
      }

      outputParameters_.channelCount = NumChannels;
      outputParameters_.sampleFormat = paInt16;
      outputParameters_.suggestedLatency = Pa_GetDeviceInfo( outputParameters_.device )->defaultLowOutputLatency;
      outputParameters_.hostApiSpecificStreamInfo = NULL;

      error_ = Pa_OpenStream( &stream_, NULL, &outputParameters_, SampleRate,
         FramesPerBuffer, paClipOff, PACallback, this );

      if ( paNoError != error_ )
      {
         CleanupPortAudio();
         return false;
      }

      error_ = Pa_StartStream( stream_ );

      if ( paNoError != error_ )
      {
         CleanupPortAudio();
         return false;
      }

      return true;
   }

   void CleanupPortAudio()
   {
      if ( stream_ )
      {
         Pa_AbortStream( stream_ );
         Pa_CloseStream( stream_ );
         stream_ = NULL;
      }

      Pa_Terminate();
   }

   static int PACallback( const void* input, void* output, unsigned long frameCount,
      const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags,
      void* userData )
   {
      Client* me = reinterpret_cast< Client* >( userData );

      if ( statusFlags )
      {
         ::PostMessage( me->hwndNotify_, RWM_PORTAUDIOSTATUS, statusFlags, 0 );
      }

      ReadBuffer* buffer = NULL;

      if ( me->startPlaying_ )
      {
         if ( me->pending_.empty() )
         {
            me->startPlaying_ = false;
            ::PostMessage( me->hwndNotify_, RWM_AUDIODROPOUT, 0, 0 );
         }
         else
         {
            buffer = me->pending_.front();
            me->pending_.pop_front();
         }
      }

      if ( buffer )
      {
         memcpy( output, buffer->data, ReadBuffer::Size );
         me->pool_.free( buffer );
      }
      else
      {
         ZeroMemory( output, frameCount * SampleSize * NumChannels );
      }

      return paContinue;
   }
};